package mya_dc.client.core;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

import mya_dc.master_server.database.ServersHashTable.ConnectInfoAndID;
import mya_dc.shared_classes.CompilationErrors;
import mya_dc.shared_classes.CompilationProject;
import mya_dc.shared_classes.ConnectInfo;
import mya_dc.shared_classes.MYA_Authentication;
import mya_dc.shared_classes.MYA_File;
import mya_dc.shared_classes.Message;
import mya_dc.shared_classes.MyaCompilationError;
import mya_dc.shared_classes.SharedDefinitions;
import mya_dc.shared_classes.Transmitter;
import mya_dc.shared_classes.MYA_FileTreeNode.FilesToGet;
import mya_dc.shared_classes.Message.EMessageType;
import mya_dc.shared_classes.SharedDefinitions.Entity;


/**
 * MYA DC compilation client
 * 
 * @author      Adam Levi
 * <br>			MYA
 */
public class CompilationClient
{
	/**
	 * Constructor
	 *
	 * @param	settings - settings to use with the client
	 */
	public CompilationClient(ClientSettings settings)
	{
		m_Settings = settings;
		m_Project = new CompilationProject(
				m_Settings.projectName,
				m_Settings.workingDirectory);
		m_CompilationErrors = new CompilationErrors();;
		m_MappedToServer = false;
		m_MappingSavedToFile = false;
	}
	
	/**
	 * Default Constructor
	 */
	public CompilationClient()
	{
		
	}
	
	
	/**
	 *
	 * @return	Compilation errors.
	 */
	public CompilationErrors getCompilationErrors()
	{
		return m_CompilationErrors;
	}
	
	/**
	 *
	 * @return	Compilation project.
	 */
	public CompilationProject getProject()
	{
		return m_Project;
	}
	
	/**
	 *
	 * @return	The client's settings
	 */
	public ClientSettings getSettings()
	{
		return m_Settings;
	}
	
	/**
	 * closes the connection
	 */
	public void closeConnection()
	{
		if (m_Transmitter != null)
			m_Transmitter.close();
	}
	
	/**
	 * A method to know if the given client is mapped, but it
	 * has yet been saved to a file.
	 * @return	true if the client is mapped to a compilation server but it wasn't saved yet, otherwise false
	 */
	public boolean isNeedSaveToFile()
	{
		return (m_MappedToServer && (m_MappingSavedToFile == false));
	}
	
	/**
	 * @return true if the client is connected to a compilation server
	 * false otherwise
	 */
	public boolean isConnected()
	{
		if (m_Transmitter == null)
			return false;
		
		return m_Transmitter.isConnected();
	}
	
	/**
	 * Connect the client to the compilation server via the master server.
	 * 
	 * @return
	 * 	<br>EMessageType.SUCCESS in case of success
	 * 	<br>EMessageType.LOGIN_FAILED_NO_MORE_SERVERS if no servers	available
	 *  <br>EMessageType.UNEXPECTED_MESSAGE in case of an unknown error.
	 * 
	 */
	public EMessageType connect()
	{
		EMessageType res = null;
		res = doConnectToMaster();
		return (res == null ? EMessageType.UNEXPECTED_MESSAGE : res);
	}
	

	/**
	 * Starts compilation process at server.
	 * 
	 * @return	the number of 'compilation packages' created at the server side,
	 *  or -1 if an error as occurred.
	 */
	public int doCompileStart() throws Exception
	{
		//transmitting compilation flags
		m_Transmitter.send(new Message(EMessageType.COMPILATION_SET_FLAGS,m_Settings.compFlags));
		
		//sending compile message
		m_Transmitter.send(new Message(EMessageType.COMPILATION_COMPILE_PROJECT,null));
		Message tmpMessage = m_Transmitter.receive();

		//first message is to know how many parts of compilation there are
		switch(tmpMessage.getType())
		{
			case COMPILATION_NUM_OF_SLAVES: { return (Integer)((tmpMessage).getObject()); }
			case COMPILATION_NO_SLAVES: return -2;
			default: { return -1; }
		}
	}
	
	/**
	 * Receives a message from the server while compiling
	 * 
	 * @return	The message received from the server while compiling.
	 */
	public Message doCompileNext() throws IOException, ClassNotFoundException
	{
		return m_Transmitter.receive();
	}
		
	/**
	 * End compilation process. This method should be called at the end of
	 * a compilation process. The method will update the compilation-errors variable
	 * if errors/warnings were found and it will also receive the Executable
	 * if such file was created.
	 * <br>
	 * <b>Note:</b> the file will be received at m_Settings.outputDirectory
	 * </br>
	 * 
	 * @return	EMessageType.SUCCESS in case of success and no errors.
	 * 			EMessageType.COMPILATION_UNABLE_TO_COMPILE if unknown error occurred
	 * 			at the server side.
	 * 			EMessageType.COMPILATION_RESULT_ERRORS if errors/warnings exists.
	 */
	public EMessageType doCompileEnd() throws IOException, ClassNotFoundException
	{
		//getting errors
		Message tmpMessage = m_Transmitter.receive();
		switch(tmpMessage.getType())
		{
			case SUCCESS: { break; }
			case COMPILATION_UNABLE_TO_COMPILE:
			case COMPILATION_RESULT_ERRORS:
				{ 
					if (tmpMessage.getObject() == null)
					{
						m_CompilationErrors = new CompilationErrors();
						break;
					}
					m_CompilationErrors = (CompilationErrors)tmpMessage.getObject();
					fixCompilationError();
					break;			
				}
			default:
		}
		//receiving file
		//updating transmitter's directory to output directory
		m_Transmitter.setWorkingDirectory(m_Settings.outputDirectory + "\\");
		Message fileMessage = m_Transmitter.receive();
		//re-updates transmitter's directory
		m_Transmitter.setWorkingDirectory(m_Settings.workingDirectory);
		
		return fileMessage.getType();
	}
	
	/**
	 * Verifies that the project at the server side does not contain
	 * extra files (for instance: files that the user deleted at past,
	 * or files from a previous project).
	 */
	public void doProjectVerification() throws IOException
	{
		m_Transmitter.send(
				new Message(
						EMessageType.PROJECT_VERIFICATION_START,
						m_Project));
		try { m_Transmitter.receive(); }
		catch (ClassNotFoundException e)
		{ e.printStackTrace(); }
	}
	
	
	/**
	 * Canceling the current operation
	 */
	public void doCancel()
	{
		try {
			m_Transmitter.send(
					new Message(
					EMessageType.OPERATION_CANCELED,
					null));
		} catch (IOException e) { }
	}
	
	/**
	 * Initialize transmission of files.
	 * 
	 * @return The number of files that will be transmitted.
	 */
	public int doTransferStart(FilesToGet type) throws IOException
	{
		m_TransmissionArray = (m_Project.getFiles(type)).toArray();
		m_LastFileTransferredIndex = 0;
		if (m_TransmissionArray.length == 0) // no new files
			return 0;
		//sending FILE_TRANSFER_START to server
		m_Transmitter.send(new Message(EMessageType.FILE_TRANSFER_START,null));
		return m_TransmissionArray.length;
	}
	
	/**
	 * Sends the next file to the compilation server.
	 * 
	 * @return The name of the file that was transmitted in this session.
	 * @throws ClassNotFoundException 
	 */
	public String doTransferNext() throws IOException, ClassNotFoundException
	{
	
		MYA_File fileToSend = (MYA_File)m_TransmissionArray[m_LastFileTransferredIndex++];
		m_Transmitter.send(new Message(EMessageType.FILE_TRANSFER,fileToSend));
		m_Transmitter.receive(); //assuming success
		fileToSend.setLastTransmitted();

		return fileToSend.toString();
	}	
	
	/**
	 * Ends the current file-transmission session.   
	 * 
	 */
	public void doTransferEnd() throws IOException
	{
		m_TransmissionArray = null;
		m_LastFileTransferredIndex = -1;
		//sending FILE_TRANSFER_DONE to server
		m_Transmitter.send(new Message(EMessageType.FILE_TRANSFER_DONE,null));
	}
	
	/**
	 * Saves the client's data at the given location
	 * 
	 * @param location - the path (including filename) to save the data to.
	 * 
	 * @return 0 - if saved successfully, -1 otherwise.
	 * 
	 */
	public int saveData(String location)
	{
		try
		{
	    	if (!m_Settings.savePassword)
	    		m_Settings.password = "";
			ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream(location));
			outStream.writeObject(m_Project);
			outStream.writeObject(m_CompilationErrors);
			outStream.writeObject(m_Settings);
			outStream.writeObject(m_MappedToServer);
			outStream.writeObject(m_MappingSavedToFile);
			outStream.flush();
			outStream.close();
			if (m_MappedToServer)
				m_MappingSavedToFile = true;
		}
		catch(Exception e)
		{
			return -1;
		}
		return 0;
	}
	
	/**
	 * Loads the client's data from the given location.
	 * 
	 * @param location - the path (including filename) to load the data from
	 * 
	 * @return 0 - if loaded successfully, -1 otherwise.
	 * 
	 */
	public int loadData(String location)
	{
		try
		{
			ObjectInputStream inStream = new ObjectInputStream(new FileInputStream(location));
			
			//loading to tmp variables	
			CompilationProject tmpCompProj	 = (CompilationProject)inStream.readObject();
			CompilationErrors tmpCompErrors  = (CompilationErrors)inStream.readObject();
			ClientSettings tmpClientSettings = (ClientSettings)inStream.readObject();
			Boolean tmpMappedToServer 		 = (Boolean)inStream.readObject();
			Boolean tmpMappingSavedTofile 	 = (Boolean)inStream.readObject();
			
			m_Project = tmpCompProj;
			m_CompilationErrors = tmpCompErrors;
			m_Settings = tmpClientSettings;
			m_MappedToServer = tmpMappedToServer;
			m_MappingSavedToFile = tmpMappingSavedTofile;
			
			inStream.close();
		}
		catch(Exception e)
		{
			return -1;
		}	
		return 0;
	}
	
	//variables
	public	Message.EMessageType m_MasterReply;
	private Transmitter			m_Transmitter;
	private CompilationProject 	m_Project;
	private CompilationErrors	m_CompilationErrors;
	private ClientSettings 		m_Settings;
	private boolean				m_MappedToServer; //true if the client is mapped to a compilation server
	private boolean				m_MappingSavedToFile; //true if (m_MappedToServer==true) && a saving to a local file has already taken place in the past
	private Object[]			m_TransmissionArray; //files to send after initTransmitFiles
	private int					m_LastFileTransferredIndex;
	
	/**
	 * Adds the project's root to each filename in m_CompilationErrors
	 */
	private void fixCompilationError()
	{
		CompilationErrors list = new CompilationErrors();
		for (MyaCompilationError error : m_CompilationErrors)
		{
			if (error.m_strFileName.equals(""))
			{
				list.add(error);
			}
			else
			{
				MyaCompilationError newError = new MyaCompilationError(
						m_Settings.workingDirectory + "\\" + error.m_strFileName,
						error.m_strLineNum,
						error.m_strError);
				list.add(newError);
			}
		}
		m_CompilationErrors = list;
	}
	
	/**
	 * Handles connecting to a compilation server via a master server.
	 * <br> <b>Works as follows:</b>
	 * <br> <b>1)</b> Connects to the master server.
	 * <br> <b>2)</b> if the client is mapped to a compilation server, and the
	 * compilation server is connected to the master, the client will receive it's
	 * address and port and it will connect to it, if the server is disconnected
	 * appropriate message will return from the master
	 * <br> <b>3)</b> if the client is not mapped to a compilation server, an ordered*
	 * list of compilation servers will return and the client will try
	 * to connect to one of the servers. if it manages to connect it will send the
	 * master the server's ID, otherwise it will tell it that it failed in connecting
	 * and the connection will be closed.
	 * <br>
	 * <br> ordered* - from the server with the least of clients mapped to it, to the
	 * servers with the biggest number of clients mapped to it.
	 * 
	 * @return SUCCESS if the client established connection with a compilation server
	 * otherwise failure messages depending on the situation.
	 */
	private EMessageType doConnectToMaster()
	{
		Transmitter tmpTransmitter = null;
		Message tmpMsg = null;
		try
		{
			tmpTransmitter = new Transmitter(
						new Socket(
							m_Settings.masterHostName,
							m_Settings.masterPort),
							null,
							SharedDefinitions.SecsBetweenTwoKeepAliveMessages);
			tmpTransmitter.send(new Message(EMessageType.LOGIN_ENTITY_IDENTIFICATION,Entity.ENTITY_CLIENT));
			while(true)
			{
				Message msgToSend = new Message(
										EMessageType.LOGIN,
										new MYA_Authentication(
												m_Settings.username,
												m_Settings.password,
												m_Settings.projectName));
				tmpTransmitter.send(msgToSend);
				tmpMsg = tmpTransmitter.receive();
				switch(tmpMsg.getType())
				{
					case LOGIN_COMPILATION_SERVER_INFO:
					{
						EMessageType result = doConnectCompServ((ConnectInfo)tmpMsg.getObject());
						switch(result)
							{
							case SUCCESS:
								{
									tmpTransmitter.send(new Message(EMessageType.SUCCESS,null));
									m_MappedToServer = true;
									tmpTransmitter.close();
									return EMessageType.SUCCESS;
								}
							case LOGIN_EXISTING:
								{
									tmpTransmitter.send(new Message(EMessageType.SUCCESS,null));
									m_MappedToServer = true;
									tmpTransmitter.close();
									return EMessageType.LOGIN_EXISTING;
								}
							default://else (deleted/dead)
								{
									tmpTransmitter.send(
											new Message(EMessageType.LOGIN_DELETE_MAPPING,null));
										continue;
								}
							}
					}
					case LOGIN_COMP_SERVER_LIST_OF_SERVERS:
					{
						//connect to compilation server until one accepts or all reject
						Long myCompServ = doConnectCompServers((Object[])tmpMsg.getObject()) ;
						if (myCompServ != null) //if success
						{
							tmpTransmitter.send(new Message(EMessageType.SUCCESS,myCompServ));
							tmpTransmitter.close();
							return EMessageType.SUCCESS;
						}
						else //all rejected
						{
							tmpTransmitter.send(
								new Message(EMessageType.LOGIN_CONNECTION_TO_ALL_SERVERS_FAILED,null));
							tmpTransmitter.close();
							return EMessageType.LOGIN_CONNECTION_TO_ALL_SERVERS_FAILED;
						}
					}
					default:
					{
						tmpTransmitter.close();
						return tmpMsg.getType(); //error of some kind
					}
				}
			}
		}
		catch(Exception e)
		{
			if (tmpTransmitter != null)
				tmpTransmitter.close();
			return EMessageType.ERROR_MASTER_SERVER_CONNECTION_FAILED;
		}
	}
	
	
	/**
	 * Tries to connect to one of the given compilation servers
	 * 
	 * @param infoArr - Array of Objects of type ConnectInfoAndID
	 * 
	 * @return the server's ID if the compilation managed to connect to 
	 * one, otherwise false.
	 */
	private Long doConnectCompServers(Object[] infoArr)
	{
		for (Object info : infoArr )
		{
			if (info instanceof ConnectInfoAndID)
			{
				ConnectInfoAndID tmpInfo = (ConnectInfoAndID) info;
				if (doConnectCompServ(tmpInfo.connectInfo) == EMessageType.SUCCESS)
					return tmpInfo.serverID;
				continue;
			}
			else
				return null;
		}
		return null;
	}
	
	/**
	 * Tries to connect to given compilation server
	 * 
	 * @param info - Connect Information of the compilation server
	 * 
	 * @return the servers return message if the server responded, null otherwise
	 */
	private EMessageType doConnectCompServ(ConnectInfo info)
	{
		try
		{
			openTransmitterToCompServer(info);
			m_Transmitter.send(new Message(EMessageType.LOGIN_ENTITY_IDENTIFICATION,Entity.ENTITY_CLIENT));
			m_Transmitter.send(new Message(
									EMessageType.LOGIN,
									new MYA_Authentication(
											m_Settings.username,
											m_Settings.password,
											m_Settings.projectName)));
			switch(m_Transmitter.receive().getType())
			{
				case SUCCESS:
				{
					return EMessageType.SUCCESS;
				}
				case LOGIN_NEW_PROJECT_SIZE:
				{
					m_Project.markTreeAsNotSent();
					m_Transmitter.send(new Message(
							EMessageType.LOGIN_NEW_PROJECT_SIZE,
							m_Project.size()));
					return m_Transmitter.receive().getType();
				}
				case LOGIN_EXISTING:
				{
					return EMessageType.LOGIN_EXISTING; 
				}
				default: throw new Exception();
			}
			
		} 
		catch (Exception e)
		{
			//e.printStackTrace();
			if (m_Transmitter != null)
				m_Transmitter.close();
			m_Transmitter = null;
			return null;
		}
	}
	
	/**
	 * opens a Transmitter to the given compilation server
	 * 
	 * @param connInfo - Connect Information of the compilation server
	 */
	private void openTransmitterToCompServer(ConnectInfo connInfo) throws IOException
	{
		m_Transmitter = new Transmitter(
				new Socket(
						connInfo.address,
						connInfo.listenPort),
						m_Settings.workingDirectory,
						SharedDefinitions.SecsBetweenTwoKeepAliveMessages);
	}
	
}
